﻿// <copyright file="LogicalExpression.cs" company="http://www.dymetis.com">
//
// Copyright (c) 2011 Eddie Z. Lin.
//
// This source is subject to the Microsoft Public License.
// See http://www.opensource.org/licenses/ms-pl.
// All other rights reserved.
// 
// </copyright>

namespace Dymetis.Expressions
{
    using System;
    using System.Linq.Expressions;
    using System.Text;
    using System.Text.RegularExpressions;

    public sealed class LogicalExpression : Expression
    {
        private readonly LambdaExpression lambda;
        private readonly LogicalExpressionSyntax syntax;
        private Delegate compiled;

        private LogicalExpression(LambdaExpression lambda, LogicalExpressionSyntax syntax)
        {
            this.lambda = lambda;
            this.syntax = syntax;
        }

        public static LogicalExpression Parse(string expression, LogicalExpressionSyntax syntax = null)
        {
            var parser = new LogicalExpressionParser(expression, syntax ?? LogicalExpressionSyntax.Default);

            return new LogicalExpression(parser.Parse(), syntax);
        }

        public bool Evaluate(IValueLookup parameters = null)
        {
            var parameterExpressions = this.lambda.Parameters;

            var paramList = new object[parameterExpressions.Count];

            if (parameters != null)
            {
                for (int i = 0; i < paramList.Length; ++i)
                {
                    paramList[i] = parameters.GetValue(parameterExpressions[i].Name);
                }
            }

            var compiled = this.compiled ?? (this.compiled = this.lambda.Compile());

            return (bool)compiled.DynamicInvoke(paramList);
        }

        public string ToString(LogicalExpressionSyntax syntax)
        {
            return new LogicalExpression(this.lambda, syntax).ToString();
        }

        public override string ToString()
        {
            var buffer = new StringBuilder();
            this.Append(buffer, this.lambda.Body);
            return buffer.ToString();
        }

        private void Append(StringBuilder buffer, Expression expression)
        {
            var binaryExpression = expression as BinaryExpression;
            var unaryExpression = expression as UnaryExpression;
            var constantExpression = expression as ConstantExpression;
            var parameterExpression = expression as ParameterExpression;
            var methodCallExpression = expression as MethodCallExpression;

            switch (expression.NodeType)
            {
                case ExpressionType.And:
                    this.Append(buffer, binaryExpression.Left, this.syntax.GetOperator(LogicalExpressionTokenType.OperatorAnd), binaryExpression.Right);
                    break;

                case ExpressionType.Or:
                    this.Append(buffer, binaryExpression.Left, this.syntax.GetOperator(LogicalExpressionTokenType.OperatorOr), binaryExpression.Right);
                    break;

                case ExpressionType.Not:
                    buffer.Append(this.syntax.GetOperator(LogicalExpressionTokenType.OperatorNot)).Append(this.syntax.GroupBegin);
                    this.Append(buffer, unaryExpression.Operand);
                    buffer.Append(this.syntax.GroupEnd);
                    break;

                case ExpressionType.Equal:
                    this.Append(buffer, binaryExpression.Left, this.syntax.GetOperator(LogicalExpressionTokenType.OperatorEqual), binaryExpression.Right, false);
                    break;

                case ExpressionType.NotEqual:
                    this.Append(buffer, binaryExpression.Left, this.syntax.GetOperator(LogicalExpressionTokenType.OperatorNotEqual), binaryExpression.Right, false);
                    break;

                case ExpressionType.LessThan:
                    this.Append(buffer, binaryExpression.Left, this.syntax.GetOperator(LogicalExpressionTokenType.OperatorLessThan), binaryExpression.Right, false);
                    break;

                case ExpressionType.LessThanOrEqual:
                    this.Append(buffer, binaryExpression.Left, this.syntax.GetOperator(LogicalExpressionTokenType.OperatorLessThanEqual), binaryExpression.Right, false);
                    break;

                case ExpressionType.GreaterThan:
                    this.Append(buffer, binaryExpression.Left, this.syntax.GetOperator(LogicalExpressionTokenType.OperatorGreaterThan), binaryExpression.Right, false);
                    break;

                case ExpressionType.GreaterThanOrEqual:
                    this.Append(buffer, binaryExpression.Left, this.syntax.GetOperator(LogicalExpressionTokenType.OperatorGreaterThanEqual), binaryExpression.Right, false);
                    break;

                case ExpressionType.Parameter:
                    buffer.Append(this.syntax.GetOperator(LogicalExpressionTokenType.VariableIndicator));
                    this.Append(buffer, parameterExpression.Name);
                    break;

                case ExpressionType.Constant:
                    if (constantExpression.Type == typeof(DateTime))
                    {
                        buffer.Append(this.syntax.GetOperator(LogicalExpressionTokenType.OperatorDate)).Append(this.syntax.GroupBegin);
                        buffer.Append(constantExpression.Value);
                        buffer.Append(this.syntax.GroupEnd);
                    }
                    else if (constantExpression.Value == null)
                    {
                        buffer.Append(this.syntax.GetOperator(LogicalExpressionTokenType.ConstantNull));
                    }
                    else
                    {
                        this.Append(buffer, constantExpression.Value.ToString());
                    }

                    break;

                case ExpressionType.Call:
                    if (methodCallExpression.Method == LogicalExpressionHelper.DateAddDaysMethod)
                    {
                        this.Append(buffer, methodCallExpression.Arguments[0], this.syntax.GetOperator(LogicalExpressionTokenType.OperatorAdd), methodCallExpression.Arguments[1], false);
                    }
                    else if (methodCallExpression.Method == LogicalExpressionHelper.DateSubstractDaysMethod)
                    {
                        this.Append(buffer, methodCallExpression.Arguments[0], this.syntax.GetOperator(LogicalExpressionTokenType.OperatorSubtract), methodCallExpression.Arguments[1], false);
                    }
                    else if (methodCallExpression.Method == LogicalExpressionHelper.StringIsMatchMethod)
                    {
                        this.Append(buffer, methodCallExpression.Arguments[0], this.syntax.GetOperator(LogicalExpressionTokenType.OperatorMatch), methodCallExpression.Arguments[1], false);
                    }
                    else if (methodCallExpression.Method == LogicalExpressionHelper.StringIsLikeMethod)
                    {
                        this.Append(buffer, methodCallExpression.Arguments[0], this.syntax.GetOperator(LogicalExpressionTokenType.OperatorLike), methodCallExpression.Arguments[1], false);
                    }
                    else if (methodCallExpression.Method == LogicalExpressionHelper.StringIsUnderMethod)
                    {
                        Append(buffer, methodCallExpression.Arguments[0], this.syntax.GetOperator(LogicalExpressionTokenType.OperatorUnder), methodCallExpression.Arguments[1], false);
                    }
                    else if (methodCallExpression.Method == LogicalExpressionHelper.DateGetDateMethod)
                    {
                        buffer.Append(this.syntax.GetOperator(LogicalExpressionTokenType.ConstantToday));
                    }
                    else if (methodCallExpression.Method == LogicalExpressionHelper.ChangeTypeMethod)
                    {
                        this.Append(buffer, methodCallExpression.Arguments[0]);
                    }

                    break;

                default:
                    throw new NotSupportedException();
            }
        }

        private void Append(StringBuilder buffer, string str)
        {
            if (Regex.IsMatch(str, @"^[\w\d]+$"))
            {
                buffer.Append(str);
            }

            if (!str.Contains(this.syntax.StringDelimiter.ToString()))
            {
                buffer.Append(this.syntax.StringDelimiter);
                buffer.Append(str);
                buffer.Append(this.syntax.StringDelimiter);
            }

            if (!str.Contains(this.syntax.AltStringDelimiter.ToString()))
            {
                buffer.Append(this.syntax.AltStringDelimiter);
                buffer.Append(str);
                buffer.Append(this.syntax.AltStringDelimiter);
            }

            throw new NotSupportedException("Invalid string constant.");
        }

        private void Append(StringBuilder buffer, Expression left, string operatorType, Expression right, bool printParenthesis = true)
        {
            if (printParenthesis)
            {
                buffer.Append(this.syntax.GroupBegin);
            }

            this.Append(buffer, left);
            if (printParenthesis)
            {
                buffer.Append(this.syntax.GroupEnd);
            }

            buffer.Append(' ').Append(operatorType).Append(' ');
            if (printParenthesis)
            {
                buffer.Append(this.syntax.GroupBegin);
            }

            this.Append(buffer, right);
            if (printParenthesis)
            {
                buffer.Append(this.syntax.GroupEnd);
            }
        }
    }
}
